﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

using Graphene.Storing;

namespace Graphene.Processing
{               
    public abstract class AbstractProcessor : MyNamed, Processor
    {        
        public abstract void MyRun();
        
        /**
         * Sets the sourceChanged variable to true, so that we know that
         * we'll have to process the new source.
         * This sourceChanged variable is used to prevent processing the same source over and
         * over if that would be the case in MyRun().
         */
        public abstract void SetSourceToChanged(Object sender, Object whatChanged);

        public void ResetNResultChanges()
        {
            lock (this.nSourceChangesLock)
            {
                this.nResultChanges = 0;
            }
        }

        public void ResetNProcessingCycles()
        {
            lock (this.nProcessingCyclesLock)
            {
                this.nProcessingCycles = 0;
            }
        }

        public override bool Equals(Object obj)
        {
            if (obj == null)
            {
                return false;
            }

            AbstractProcessor ap = obj as AbstractProcessor;
            if ((System.Object)ap == null)
            {
                return false;
            }

            return myId.Equals(ap.MyId);
        }

        public override int GetHashCode()
        {
            return myId.GetHashCode();
        } 

        public override string ToString()
        {
            return myName;
        }        
        
        public abstract void AddResultChangedHandler(Graphene.Processing.ResultChangedHandler handler);

        public abstract void RemoveResultChangedHandler(Graphene.Processing.ResultChangedHandler handler);

        public abstract void AddProcessedHandler(Graphene.Processing.ProcessedHandler handler);

        public abstract void RemoveProcessedHandler(Graphene.Processing.ProcessedHandler handler);


        /**
         * Static methods:
         */

        /**
         * Converts a Bitmap image to a byte array.
         * About the coordinates:
         * bmp.GetPixel(x, y).R is equivalent to bmpBytes[3 * (bmp.Width * y + x) + 2]
         * bmp.GetPixel(x, y).G is equivalent to bmpBytes[3 * (bmp.Width * y + x) + 1]
         * bmp.GetPixel(x, y).B is equivalent to bmpBytes[3 * (bmp.Width * y + x)]
         * 
         * Very fast method. Does under 1 ms. Built because bmp.GetPixel() is very very slow. Did over 300 ms.
         * Without this, the application could not function.
         * 
         * Copies out the exactly same bytes as they are in the memory for the input variable.
         * Originally written by MrPolite.
         * Source found at http://www.vbforums.com/showthread.php?t=358917
         */
        public static unsafe byte[] BitmapToByteImage_Unsafe(Bitmap bmp)
        {
            System.Drawing.Imaging.BitmapData bData = bmp.LockBits(new Rectangle(new System.Drawing.Point(), bmp.Size), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
            int byteCount = bData.Stride * bmp.Height;
            byte[] bmpBytes = new byte[byteCount];
            Marshal.Copy(bData.Scan0, bmpBytes, 0, byteCount);
            bmp.UnlockBits(bData);

            return bmpBytes;
        }

        /**
         * Converts a byte array containing an image to a Bitmap image.
         * Coordinate conversion is the same as in BitmapToByteImage_Unsafe().
         * 
         * Very fast method.         
         *         
         * Originally written by MrPolite.
         * Source found at http://www.vbforums.com/showthread.php?t=358917
         */
        public static unsafe Bitmap ByteImageToBitmap_Unsafe(byte[] bmpBytes, int sourceWidth, int sourceHeight)
        {
            if (bmpBytes == null) return null;

            Bitmap bmp = new Bitmap(sourceWidth, sourceHeight);
            BitmapData bData = bmp.LockBits(new Rectangle(new System.Drawing.Point(), bmp.Size), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
            Marshal.Copy(bmpBytes, 0, bData.Scan0, bmpBytes.Length);
            bmp.UnlockBits(bData);

            return bmp;
        }

        /**
         * Compares two colors by their RGB components.
         * Two colors are similar if the Manhattan distance between them
         * in the RGB color space is not greater than the tolerance. 
         *
         * @param  rA   red component of color A
         * @param  gA   green component of color A
         * @param  bA   blue component of color A
         * @param  rB   red component of color B
         * @param  gB   green component of color B
         * @param  bB   blue component of color B
         * @param  tolerance allowed shade difference to be declared the same color
         * @return      true if they are of similar color, false otherwise
         */
        public static bool CompareColor(byte rA, byte gA, byte bA, byte rB, byte gB, byte bB, int tolerance)
        {
            int diffRed = rA - rB;
            if ((diffRed > tolerance) || (diffRed < -tolerance))
            {
                return false;
            }
            int diffGreen = gA - gB;
            if ((diffGreen > tolerance) || (diffGreen < -tolerance))
            {
                return false;
            }
            int diffBlue = bA - bB;
            if ((diffBlue > tolerance) || (diffBlue < -tolerance))
            {
                return false;
            }

            return true;
        }

        public static bool CompareColor(Color colorA, Color colorB, int tolerance)
        {
            byte rA = colorA.R;
            byte gA = colorA.G;
            byte bA = colorA.B;
            byte rB = colorB.R;
            byte gB = colorB.G;
            byte bB = colorB.B;

            int diffRed = rA - rB;
            if ((diffRed > tolerance) || (diffRed < -tolerance))
            {
                return false;
            }
            int diffGreen = gA - gB;
            if ((diffGreen > tolerance) || (diffGreen < -tolerance))
            {
                return false;
            }
            int diffBlue = bA - bB;
            if ((diffBlue > tolerance) || (diffBlue < -tolerance))
            {
                return false;
            }

            return true;
        }

        public static byte[] DrawX(byte[] source, int width, int height, Graphene.Storing.PointInt2D point, Color color)
        {
            for (int i = -3; i <= 3; i++)
            {
                if (point.X + i >= 0 && point.X + i < width
                    && point.Y + i >= 0 && point.Y + i < height)
                {
                    int addr = 3 * (width * (point.Y + i) + point.X + i);
                    source[addr + 2] = color.R;
                    source[addr + 1] = color.G;
                    source[addr] = color.B;
                }
                if (point.X + i >= 0 && point.X + i < width
                    && point.Y - i >= 0 && point.Y - i < height)
                {
                    int addr = 3 * (width * (point.Y - i) + point.X + i);
                    source[addr + 2] = color.R;
                    source[addr + 1] = color.G;
                    source[addr] = color.B;
                }
            }

            return source;
        }

        public static byte[] Resize(byte[] source, int sourceWidth, int sourceHeight, int destinationWidth, int destinationHeight)
        {

            if ((source == null) || (destinationWidth <= 0) || (destinationHeight <= 0))
            {
                return null;
            }

            byte[] destination = new byte[destinationWidth * destinationHeight * 3];
            double dx, dy;

            dx = (double)sourceWidth / destinationWidth;
            dy = (double)sourceHeight / destinationHeight;

            double sourceYDouble = 0;
            int sourceYInt = 0;
            int destinationYInt = 0;
            while (destinationYInt < destinationHeight)
            {
                double sourceXDouble = 0;
                int sourceXInt = 0;
                int destinationXInt = 0;
                while (destinationXInt < destinationWidth)
                {
                    int sourceAddress = 3 * ((sourceWidth * sourceYInt) + sourceXInt);
                    int destinationAddress = 3 * ((destinationWidth * destinationYInt) + destinationXInt);
                    destination[destinationAddress] = source[sourceAddress];
                    destination[destinationAddress + 1] = source[sourceAddress + 1];
                    destination[destinationAddress + 2] = source[sourceAddress + 2];

                    sourceXDouble += dx;
                    sourceXInt = (int)sourceXDouble;
                    destinationXInt++;
                }
                sourceYDouble += dy;
                sourceYInt = (int)sourceYDouble;
                destinationYInt++;
            }

            return destination;
        }

        public static byte[] ResizeAndFadeToColor(byte[] source, int sourceWidth, int sourceHeight, int destinationWidth, int destinationHeight, Color fadeToColor, double alpha)
        {
            if ((source == null) || (destinationWidth <= 0) || (destinationHeight <= 0))
            {
                return null;
            }

            byte[] destination = new byte[destinationWidth * destinationHeight * 3];

            double dx = (double)sourceWidth / destinationWidth;
            double dy = (double)sourceHeight / destinationHeight;

            byte fadeToColorR = fadeToColor.R;
            byte fadeToColorG = fadeToColor.G;
            byte fadeToColorB = fadeToColor.B;

            double sourceYDouble = 0;
            int sourceYInt = 0;
            int destinationYInt = 0;
            while (destinationYInt < destinationHeight)
            {
                double sourceXDouble = 0;
                int sourceXInt = 0;
                int destinationXInt = 0;
                while (destinationXInt < destinationWidth)
                {
                    int sourceAddress = 3 * ((sourceWidth * sourceYInt) + sourceXInt);
                    int destinationAddress = 3 * ((destinationWidth * destinationYInt) + destinationXInt);

                    destination[destinationAddress] = (byte)(source[sourceAddress] * alpha + fadeToColorB * (1 - alpha));
                    destination[destinationAddress + 1] = (byte)(source[sourceAddress + 1] * alpha + fadeToColorG * (1 - alpha));
                    destination[destinationAddress + 2] = (byte)(source[sourceAddress + 2] * alpha + fadeToColorR * (1 - alpha));

                    sourceXDouble += dx;
                    sourceXInt = (int)sourceXDouble;
                    destinationXInt++;
                }
                sourceYDouble += dy;
                sourceYInt = (int)sourceYDouble;
                destinationYInt++;
            }

            return destination;
        }

        public static byte[] BoolImageOverByteImage(bool[,] source, byte[] backgroundByteImage, Color foregroundColor, double alpha, int width, int height)
        {
            if (alpha < 0 || alpha > 1)
            {
                return backgroundByteImage;
            }

            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    int addr = 3 * (y * width + x);
                    if (source[y, x])
                    {
                        backgroundByteImage[addr] = (byte)(backgroundByteImage[addr] * (1 - alpha) + foregroundColor.B * alpha);
                        backgroundByteImage[addr + 1] = (byte)(backgroundByteImage[addr + 1] * (1 - alpha) + foregroundColor.G * alpha);
                        backgroundByteImage[addr + 2] = (byte)(backgroundByteImage[addr + 2] * (1 - alpha) + foregroundColor.R * alpha);
                    }
                }
            }

            return backgroundByteImage;
        }

        /**
         * Draws a spat on top of a ByteImage.
         */
        public static byte[] SpatOverByteImage(Spat source, byte[] backgroundByteImage, int width, int height, Color spatColor, double alpha)
        {
            if (source == null || alpha <= 0 || alpha > 1)
            {
                return backgroundByteImage;
            }

            byte spatRed = spatColor.R;
            byte spatGreen = spatColor.G;
            byte spatBlue = spatColor.B;

            foreach (Graphene.Storing.PointInt2D point in source)
            {
                int addr = 3 * (point.Y * width + point.X);
                int x = point.X;

                backgroundByteImage[addr] = (byte)(backgroundByteImage[addr] * (1 - alpha) + spatBlue * alpha);
                backgroundByteImage[addr + 1] = (byte)(backgroundByteImage[addr + 1] * (1 - alpha) + spatGreen * alpha);
                backgroundByteImage[addr + 2] = (byte)(backgroundByteImage[addr + 2] * (1 - alpha) + spatRed * alpha);
            }

            return backgroundByteImage;
        }

        /**
         * Multiplies the image with a value in [0, 1).
         */
        public static byte[] DarkenByteImage(byte[] source, int width, int height, double rate)
        {
            if (rate < 0 || rate >= 1)
            {
                return source;
            }

            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    for (int c = 0; c < 3; c++)
                    {
                        int addr = 3 * (y * width + x) + c;
                        source[addr] = (byte)((double)source[addr] * rate);
                    }
                }
            }

            return source;
        }

        /* Highlight pixels that are similar to a color (fall into the tolerance proximity) and blacken those that not. */
        public static byte[] FilterColor(byte[] source, int sourceWidth, int sourceHeight, Color filteredColor, Color foreColor, Color backColor, byte shadeTolerance)
        {
            if (source == null || shadeTolerance < 0 || sourceWidth < 0 || sourceHeight < 0)
            {
                return null;
            }

            byte[] destination = new byte[sourceWidth * sourceHeight * 3];
            double dx, dy;

            for (int y = 0; y < sourceHeight; y++)
            {
                for (int x = 0; x < sourceWidth; x++)
                {
                    int address = 3 * ((sourceWidth * y) + x);
                    if (AbstractProcessor.CompareColor(source[address + 2], source[address + 1], source[address], filteredColor.R, filteredColor.G, filteredColor.B, shadeTolerance))
                    {
                        destination[address] = foreColor.B;
                        destination[address + 1] = foreColor.G;
                        destination[address + 2] = foreColor.R;
                    }
                    else
                    {
                        destination[address] = backColor.B;
                        destination[address + 1] = backColor.G;
                        destination[address + 2] = backColor.R;
                    }
                }
            }

            return destination;
        }

        /* Highlight pixels that are similar to a color (fall into the tolerance proximity) and blacken those that not.
         * One good contigious spat is good. The rest that is similar will be highlighted with another color as wrong. */
        public static float HighlightMarkerFromBackground(
            byte[] source,
            byte[] destination,
            PointInt2D sourceSize,
            PointInt2D goodPosition,
            Color filteredColor,
            Color goodForeColor,
            Color badForeColor,
            Color backColor,
            byte searchShadeTolerance,
            byte extendShadeTolerance
            )
        {
            short[] NEIGHBOURS_8_X = { -1, 0, 1, -1, 1, -1, 0, 1 };
            short[] NEIGHBOURS_8_Y = { -1, -1, 1, 0, 0, 1, 1, 1 };
            if (source == null || goodPosition == null || sourceSize == null
                || searchShadeTolerance < 0 || sourceSize.X < 0 || sourceSize.Y < 0)
            {
                return -1.0f;
            }

            bool[,] visited = new bool[sourceSize.Y, sourceSize.X];
            for (int y = 0; y < sourceSize.Y; y++)
            {
                for (int x = 0; x < sourceSize.X; x++)
                {
                    visited[y, x] = false;
                }
            }
            bool[,] queued = new bool[sourceSize.Y, sourceSize.X];
            for (int y = 0; y < sourceSize.Y; y++)
            {
                for (int x = 0; x < sourceSize.X; x++)
                {
                    queued[y, x] = false;
                }
            }

            byte searchedR = filteredColor.R;
            byte searchedG = filteredColor.G;
            byte searchedB = filteredColor.B;                        
            Queue<PointInt2D> queue = new Queue<PointInt2D>();
            queue.Enqueue(goodPosition);
            while (queue.Count > 0)
            {
                PointInt2D head = queue.Dequeue();                
                int px = head.X;
                int py = head.Y;
                visited[py, px] = true;
                for (int i = 0; i < 8; i++)
                {
                    int neighbourX = px + NEIGHBOURS_8_X[i];
                    int neighbourY = py + NEIGHBOURS_8_Y[i];
                    PointInt2D neighbour = new PointInt2D(neighbourX, neighbourY);
                    if ((neighbourX >= 0) && (neighbourX < sourceSize.X) && (neighbourY >= 0) && (neighbourY < sourceSize.Y)
                            && (!visited[neighbourY, neighbourX]) && (!queued[neighbour.Y, neighbour.X]))
                    {
                        int byteAddress = 3 * (neighbourY * sourceSize.X + neighbourX);
                        byte foundRed = source[byteAddress + 2];
                        byte foundGreen = source[byteAddress + 1];
                        byte foundBlue = source[byteAddress];
                        if (AbstractProcessor.CompareColor(searchedR, searchedG, searchedB, foundRed, foundGreen, foundBlue, extendShadeTolerance))
                        {
                            queued[neighbourY, neighbourX] = true;
                            queue.Enqueue(neighbour);
                        }
                    }
                }
            }
            
            int nBad = 0;
            for (int y = 0; y < sourceSize.Y; y++)
            {
                for (int x = 0; x < sourceSize.X; x++)
                {
                    int address = 3 * ((sourceSize.X * y) + x);
                    if (visited[y, x])
                    {
                        destination[address] = goodForeColor.B;
                        destination[address + 1] = goodForeColor.G;
                        destination[address + 2] = goodForeColor.R;
                    }
                    else
                    {
                        if (AbstractProcessor.CompareColor(source[address + 2], source[address + 1], source[address], filteredColor.R, filteredColor.G, filteredColor.B, searchShadeTolerance))
                        {
                            destination[address] = badForeColor.B;
                            destination[address + 1] = badForeColor.G;
                            destination[address + 2] = badForeColor.R;
                            nBad++;
                        }
                        else
                        {
                            destination[address] = backColor.B;
                            destination[address + 1] = backColor.G;
                            destination[address + 2] = backColor.R;
                        }
                    }
                }
            }

            return (float)nBad / (sourceSize.X * sourceSize.Y);
        }

        /* Getters/setters: */

        public string MyId
        {
            get { return this.myId; }
        }
        
        public string MyName
        {
            get { return this.myName; }
        }

        public Object ResultData
        {
            get { return result; }
        }        
        
        public double AverageProcessTime
        {
            get { return averageProcessTime; }
        }
        
        public int LongestProcessTime
        {
            get { return longestProcessTime; }
        }
        
        public int NProcessingCycles
        {
            get { return nProcessingCycles; }            
        }
        
        public int NResultChanges
        {
            get { return nResultChanges; }
        }

        /* Attributes: */

        protected readonly Object nProcessingCyclesLock = new Object();
        protected readonly Object nSourceChangesLock = new Object();

        protected Model model;
        protected string myId;
        protected string myName;

        /**
         * We already know that this is derived into a List of Markers.
         */
        protected Object result;

        /**
         * Marks which part of the result changed in the last processing cycle.
         */
        protected Object resultChanged;

        protected double averageProcessTime;
        protected int longestProcessTime;
        protected int nProcessingCycles;
        protected int nResultChanges;        
    }
}
